/*
 * Copyright 2002-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.beans.factory.groovy;

import groovy.lang.*;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
import org.springframework.beans.factory.parsing.Location;
import org.springframework.beans.factory.parsing.Problem;
import org.springframework.beans.factory.support.*;
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
import org.springframework.beans.factory.xml.NamespaceHandler;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.beans.factory.xml.XmlReaderContext;
import org.springframework.core.io.DescriptiveResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.*;

/**
 * A Groovy-based reader for Spring bean definitions: like a Groovy builder,
 * but more of a DSL for Spring configuration.
 *
 * <p>This bean definition reader also understands XML bean definition files,
 * allowing for seamless mixing and matching with Groovy bean definition files.
 *
 * <p>Typically applied to a
 * {@link org.springframework.beans.factory.support.DefaultListableBeanFactory}
 * or a {@link org.springframework.context.support.GenericApplicationContext},
 * but can be used against any {@link BeanDefinitionRegistry} implementation.
 *
 * <h3>Example Syntax</h3>
 * <pre class="code">
 * import org.hibernate.SessionFactory
 * import org.apache.commons.dbcp.BasicDataSource
 *
 * def reader = new GroovyBeanDefinitionReader(myApplicationContext)
 * reader.beans {
 *     dataSource(BasicDataSource) {                  // <--- invokeMethod
 *         driverClassName = "org.hsqldb.jdbcDriver"
 *         url = "jdbc:hsqldb:mem:grailsDB"
 *         username = "sa"                            // <-- setProperty
 *         password = ""
 *         settings = [mynew:"setting"]
 *     }
 *     sessionFactory(SessionFactory) {
 *         dataSource = dataSource                    // <-- getProperty for retrieving references
 *     }
 *     myService(MyService) {
 *         nestedBean = { AnotherBean bean ->         // <-- setProperty with closure for nested bean
 *             dataSource = dataSource
 *         }
 *     }
 * }</pre>
 *
 * <p>You can also load resources containing beans defined in a Groovy script using
 * either the {@link #loadBeanDefinitions(Resource...)} or
 * {@link #loadBeanDefinitions(String...)} method, with a script looking similar to
 * the following.
 *
 * <pre class="code">
 * import org.hibernate.SessionFactory
 * import org.apache.commons.dbcp.BasicDataSource
 *
 * beans {
 *     dataSource(BasicDataSource) {
 *         driverClassName = "org.hsqldb.jdbcDriver"
 *         url = "jdbc:hsqldb:mem:grailsDB"
 *         username = "sa"
 *         password = ""
 *         settings = [mynew:"setting"]
 *     }
 *     sessionFactory(SessionFactory) {
 *         dataSource = dataSource
 *     }
 *     myService(MyService) {
 *         nestedBean = { AnotherBean bean ->
 *             dataSource = dataSource
 *         }
 *     }
 * }</pre>
 *
 * @author Jeff Brown
 * @author Graeme Rocher
 * @author Juergen Hoeller
 * @author Sam Brannen
 * @see BeanDefinitionRegistry
 * @see org.springframework.beans.factory.support.DefaultListableBeanFactory
 * @see org.springframework.context.support.GenericApplicationContext
 * @see org.springframework.context.support.GenericGroovyApplicationContext
 * @since 4.0
 */
public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader implements GroovyObject {

    /**
     * Standard {@code XmlBeanDefinitionReader} created with default
     * settings for loading bean definitions from XML files.
     */
    private final XmlBeanDefinitionReader standardXmlBeanDefinitionReader;

    /**
     * Groovy DSL {@code XmlBeanDefinitionReader} for loading bean definitions
     * via the Groovy DSL, typically configured with XML validation disabled.
     */
    private final XmlBeanDefinitionReader groovyDslXmlBeanDefinitionReader;

    private final Map<String, String> namespaces = new HashMap<>();

    private final Map<String, DeferredProperty> deferredProperties = new HashMap<>();

    private MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(getClass());

    private Binding binding;

    private GroovyBeanDefinitionWrapper currentBeanDefinition;


    /**
     * Create a new {@code GroovyBeanDefinitionReader} for the given
     * {@link BeanDefinitionRegistry}.
     *
     * @param registry the {@code BeanDefinitionRegistry} to load bean definitions into
     */
    public GroovyBeanDefinitionReader(BeanDefinitionRegistry registry) {
        super(registry);
        this.standardXmlBeanDefinitionReader = new XmlBeanDefinitionReader(registry);
        this.groovyDslXmlBeanDefinitionReader = new XmlBeanDefinitionReader(registry);
        this.groovyDslXmlBeanDefinitionReader.setValidating(false);
    }

    /**
     * Create a new {@code GroovyBeanDefinitionReader} based on the given
     * {@link XmlBeanDefinitionReader}, loading bean definitions into its
     * {@code BeanDefinitionRegistry} and delegating Groovy DSL loading to it.
     * <p>The supplied {@code XmlBeanDefinitionReader} should typically
     * be pre-configured with XML validation disabled.
     *
     * @param xmlBeanDefinitionReader the {@code XmlBeanDefinitionReader} to
     *                                derive the registry from and to delegate Groovy DSL loading to
     */
    public GroovyBeanDefinitionReader(XmlBeanDefinitionReader xmlBeanDefinitionReader) {
        super(xmlBeanDefinitionReader.getRegistry());
        this.standardXmlBeanDefinitionReader = new XmlBeanDefinitionReader(xmlBeanDefinitionReader.getRegistry());
        this.groovyDslXmlBeanDefinitionReader = xmlBeanDefinitionReader;
    }

    @Override
    public MetaClass getMetaClass() {
        return this.metaClass;
    }

    @Override
    public void setMetaClass(MetaClass metaClass) {
        this.metaClass = metaClass;
    }

    /**
     * Return a specified binding for Groovy variables, if any.
     */
    public Binding getBinding() {
        return this.binding;
    }

    /**
     * Set the binding, i.e. the Groovy variables available in the scope
     * of a {@code GroovyBeanDefinitionReader} closure.
     */
    public void setBinding(Binding binding) {
        this.binding = binding;
    }


    // TRADITIONAL BEAN DEFINITION READER METHODS

    /**
     * Load bean definitions from the specified Groovy script or XML file.
     * <p>Note that {@code ".xml"} files will be parsed as XML content; all other kinds
     * of resources will be parsed as Groovy scripts.
     *
     * @param resource the resource descriptor for the Groovy script or XML file
     * @return the number of bean definitions found
     * @throws BeanDefinitionStoreException in case of loading or parsing errors
     */
    @Override
    public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
        return loadBeanDefinitions(new EncodedResource(resource));
    }

    /**
     * Load bean definitions from the specified Groovy script or XML file.
     * <p>Note that {@code ".xml"} files will be parsed as XML content; all other kinds
     * of resources will be parsed as Groovy scripts.
     *
     * @param encodedResource the resource descriptor for the Groovy script or XML file,
     *                        allowing specification of an encoding to use for parsing the file
     * @return the number of bean definitions found
     * @throws BeanDefinitionStoreException in case of loading or parsing errors
     */
    public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
        // Check for XML files and redirect them to the "standard" XmlBeanDefinitionReader
        String filename = encodedResource.getResource().getFilename();
        if (StringUtils.endsWithIgnoreCase(filename, ".xml")) {
            return this.standardXmlBeanDefinitionReader.loadBeanDefinitions(encodedResource);
        }

        if (logger.isTraceEnabled()) {
            logger.trace("Loading Groovy bean definitions from " + encodedResource);
        }

        @SuppressWarnings("serial")
        Closure<Object> beans = new Closure<Object>(this) {
            @Override
            public Object call(Object... args) {
                invokeBeanDefiningClosure((Closure<?>) args[0]);
                return null;
            }
        };
        Binding binding = new Binding() {
            @Override
            public void setVariable(String name, Object value) {
                if (currentBeanDefinition != null) {
                    applyPropertyToBeanDefinition(name, value);
                }
                else {
                    super.setVariable(name, value);
                }
            }
        };
        binding.setVariable("beans", beans);

        int countBefore = getRegistry().getBeanDefinitionCount();
        try {
            GroovyShell shell = new GroovyShell(getBeanClassLoader(), binding);
            shell.evaluate(encodedResource.getReader(), "beans");
        }
        catch (Throwable ex) {
            throw new BeanDefinitionParsingException(new Problem("Error evaluating Groovy script: " + ex.getMessage(),
                    new Location(encodedResource.getResource()), null, ex));
        }

        int count = getRegistry().getBeanDefinitionCount() - countBefore;
        if (logger.isDebugEnabled()) {
            logger.debug("Loaded " + count + " bean definitions from " + encodedResource);
        }
        return count;
    }


    // METHODS FOR CONSUMPTION IN A GROOVY CLOSURE

    /**
     * Defines a set of beans for the given block or closure.
     *
     * @param closure the block or closure
     * @return this {@code GroovyBeanDefinitionReader} instance
     */
    public GroovyBeanDefinitionReader beans(Closure<?> closure) {
        return invokeBeanDefiningClosure(closure);
    }

    /**
     * Define an inner bean definition.
     *
     * @param type the bean type
     * @return the bean definition
     */
    public GenericBeanDefinition bean(Class<?> type) {
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(type);
        return beanDefinition;
    }

    /**
     * Define an inner bean definition.
     *
     * @param type the bean type
     * @param args the constructors arguments and closure configurer
     * @return the bean definition
     */
    public AbstractBeanDefinition bean(Class<?> type, Object... args) {
        GroovyBeanDefinitionWrapper current = this.currentBeanDefinition;
        try {
            Closure<?> callable = null;
            Collection<Object> constructorArgs = null;
            if (!ObjectUtils.isEmpty(args)) {
                int index = args.length;
                Object lastArg = args[index - 1];
                if (lastArg instanceof Closure<?>) {
                    callable = (Closure<?>) lastArg;
                    index--;
                }
                constructorArgs = resolveConstructorArguments(args, 0, index);
            }
            this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(null, type, constructorArgs);
            if (callable != null) {
                callable.call(this.currentBeanDefinition);
            }
            return this.currentBeanDefinition.getBeanDefinition();
        }
        finally {
            this.currentBeanDefinition = current;
        }
    }

    /**
     * Define a Spring XML namespace definition to use.
     *
     * @param definition the namespace definition
     */
    public void xmlns(Map<String, String> definition) {
        if (!definition.isEmpty()) {
            for (Map.Entry<String, String> entry : definition.entrySet()) {
                String namespace = entry.getKey();
                String uri = entry.getValue();
                if (uri == null) {
                    throw new IllegalArgumentException("Namespace definition must supply a non-null URI");
                }
                NamespaceHandler namespaceHandler =
                        this.groovyDslXmlBeanDefinitionReader.getNamespaceHandlerResolver().resolve(uri);
                if (namespaceHandler == null) {
                    throw new BeanDefinitionParsingException(new Problem("No namespace handler found for URI: " + uri,
                            new Location(new DescriptiveResource(("Groovy")))));
                }
                this.namespaces.put(namespace, uri);
            }
        }
    }

    /**
     * Import Spring bean definitions from either XML or Groovy sources into the
     * current bean builder instance.
     *
     * @param resourcePattern the resource pattern
     */
    public void importBeans(String resourcePattern) throws IOException {
        loadBeanDefinitions(resourcePattern);
    }


    // INTERNAL HANDLING OF GROOVY CLOSURES AND PROPERTIES

    /**
     * This method overrides method invocation to create beans for each method name that
     * takes a class argument.
     */
    @Override
    public Object invokeMethod(String name, Object arg) {
        Object[] args = (Object[]) arg;
        if ("beans".equals(name) && args.length == 1 && args[0] instanceof Closure) {
            return beans((Closure<?>) args[0]);
        }
        else if ("ref".equals(name)) {
            String refName;
            if (args[0] == null) {
                throw new IllegalArgumentException("Argument to ref() is not a valid bean or was not found");
            }
            if (args[0] instanceof RuntimeBeanReference) {
                refName = ((RuntimeBeanReference) args[0]).getBeanName();
            }
            else {
                refName = args[0].toString();
            }
            boolean parentRef = false;
            if (args.length > 1 && args[1] instanceof Boolean) {
                parentRef = (Boolean) args[1];
            }
            return new RuntimeBeanReference(refName, parentRef);
        }
        else if (this.namespaces.containsKey(name) && args.length > 0 && args[0] instanceof Closure) {
            GroovyDynamicElementReader reader = createDynamicElementReader(name);
            reader.invokeMethod("doCall", args);
        }
        else if (args.length > 0 && args[0] instanceof Closure) {
            // abstract bean definition
            return invokeBeanDefiningMethod(name, args);
        }
        else if (args.length > 0 &&
                (args[0] instanceof Class || args[0] instanceof RuntimeBeanReference || args[0] instanceof Map)) {
            return invokeBeanDefiningMethod(name, args);
        }
        else if (args.length > 1 && args[args.length - 1] instanceof Closure) {
            return invokeBeanDefiningMethod(name, args);
        }
        MetaClass mc = DefaultGroovyMethods.getMetaClass(getRegistry());
        if (!mc.respondsTo(getRegistry(), name, args).isEmpty()) {
            return mc.invokeMethod(getRegistry(), name, args);
        }
        return this;
    }

    private boolean addDeferredProperty(String property, Object newValue) {
        if (newValue instanceof List || newValue instanceof Map) {
            this.deferredProperties.put(this.currentBeanDefinition.getBeanName() + '.' + property,
                    new DeferredProperty(this.currentBeanDefinition, property, newValue));
            return true;
        }
        return false;
    }

    private void finalizeDeferredProperties() {
        for (DeferredProperty dp : this.deferredProperties.values()) {
            if (dp.value instanceof List) {
                dp.value = manageListIfNecessary((List<?>) dp.value);
            }
            else if (dp.value instanceof Map) {
                dp.value = manageMapIfNecessary((Map<?, ?>) dp.value);
            }
            dp.apply();
        }
        this.deferredProperties.clear();
    }

    /**
     * When a method argument is only a closure it is a set of bean definitions.
     *
     * @param callable the closure argument
     * @return this {@code GroovyBeanDefinitionReader} instance
     */
    protected GroovyBeanDefinitionReader invokeBeanDefiningClosure(Closure<?> callable) {
        callable.setDelegate(this);
        callable.call();
        finalizeDeferredProperties();
        return this;
    }

    /**
     * This method is called when a bean definition node is called.
     *
     * @param beanName the name of the bean to define
     * @param args     the arguments to the bean. The first argument is the class name, the last
     *                 argument is sometimes a closure. All the arguments in between are constructor arguments.
     * @return the bean definition wrapper
     */
    private GroovyBeanDefinitionWrapper invokeBeanDefiningMethod(String beanName, Object[] args) {
        boolean hasClosureArgument = (args[args.length - 1] instanceof Closure);
        if (args[0] instanceof Class) {
            Class<?> beanClass = (Class<?>) args[0];
            if (hasClosureArgument) {
                if (args.length - 1 != 1) {
                    this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(
                            beanName, beanClass, resolveConstructorArguments(args, 1, args.length - 1));
                }
                else {
                    this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, beanClass);
                }
            }
            else {
                this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(
                        beanName, beanClass, resolveConstructorArguments(args, 1, args.length));
            }
        }
        else if (args[0] instanceof RuntimeBeanReference) {
            this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName);
            this.currentBeanDefinition.getBeanDefinition().setFactoryBeanName(((RuntimeBeanReference) args[0]).getBeanName());
        }
        else if (args[0] instanceof Map) {
            // named constructor arguments
            if (args.length > 1 && args[1] instanceof Class) {
                List<Object> constructorArgs =
                        resolveConstructorArguments(args, 2, hasClosureArgument ? args.length - 1 : args.length);
                this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, (Class<?>) args[1], constructorArgs);
                Map<?, ?> namedArgs = (Map<?, ?>) args[0];
                for (Object o : namedArgs.keySet()) {
                    String propName = (String) o;
                    setProperty(propName, namedArgs.get(propName));
                }
            }
            // factory method syntax
            else {
                this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName);
                // First arg is the map containing factoryBean : factoryMethod
                Map.Entry<?, ?> factoryBeanEntry = ((Map<?, ?>) args[0]).entrySet().iterator().next();
                // If we have a closure body, that will be the last argument.
                // In between are the constructor args
                int constructorArgsTest = (hasClosureArgument ? 2 : 1);
                // If we have more than this number of args, we have constructor args
                if (args.length > constructorArgsTest) {
                    // factory-method requires args
                    int endOfConstructArgs = (hasClosureArgument ? args.length - 1 : args.length);
                    this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, null,
                            resolveConstructorArguments(args, 1, endOfConstructArgs));
                }
                else {
                    this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName);
                }
                this.currentBeanDefinition.getBeanDefinition().setFactoryBeanName(factoryBeanEntry.getKey().toString());
                this.currentBeanDefinition.getBeanDefinition().setFactoryMethodName(factoryBeanEntry.getValue().toString());
            }

        }
        else if (args[0] instanceof Closure) {
            this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName);
            this.currentBeanDefinition.getBeanDefinition().setAbstract(true);
        }
        else {
            List<Object> constructorArgs =
                    resolveConstructorArguments(args, 0, hasClosureArgument ? args.length - 1 : args.length);
            this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(beanName, null, constructorArgs);
        }

        if (hasClosureArgument) {
            Closure<?> callable = (Closure<?>) args[args.length - 1];
            callable.setDelegate(this);
            callable.setResolveStrategy(Closure.DELEGATE_FIRST);
            callable.call(this.currentBeanDefinition);
        }

        GroovyBeanDefinitionWrapper beanDefinition = this.currentBeanDefinition;
        this.currentBeanDefinition = null;
        beanDefinition.getBeanDefinition().setAttribute(GroovyBeanDefinitionWrapper.class.getName(), beanDefinition);
        getRegistry().registerBeanDefinition(beanName, beanDefinition.getBeanDefinition());
        return beanDefinition;
    }

    protected List<Object> resolveConstructorArguments(Object[] args, int start, int end) {
        Object[] constructorArgs = Arrays.copyOfRange(args, start, end);
        for (int i = 0; i < constructorArgs.length; i++) {
            if (constructorArgs[i] instanceof GString) {
                constructorArgs[i] = constructorArgs[i].toString();
            }
            else if (constructorArgs[i] instanceof List) {
                constructorArgs[i] = manageListIfNecessary((List<?>) constructorArgs[i]);
            }
            else if (constructorArgs[i] instanceof Map) {
                constructorArgs[i] = manageMapIfNecessary((Map<?, ?>) constructorArgs[i]);
            }
        }
        return Arrays.asList(constructorArgs);
    }

    /**
     * Checks whether there are any {@link RuntimeBeanReference RuntimeBeanReferences}
     * inside the {@link Map} and converts it to a {@link ManagedMap} if necessary.
     *
     * @param map the original Map
     * @return either the original map or a managed copy of it
     */
    private Object manageMapIfNecessary(Map<?, ?> map) {
        boolean containsRuntimeRefs = false;
        for (Object element : map.values()) {
            if (element instanceof RuntimeBeanReference) {
                containsRuntimeRefs = true;
                break;
            }
        }
        if (containsRuntimeRefs) {
            Map<Object, Object> managedMap = new ManagedMap<>();
            managedMap.putAll(map);
            return managedMap;
        }
        return map;
    }

    /**
     * Checks whether there are any {@link RuntimeBeanReference RuntimeBeanReferences}
     * inside the {@link List} and converts it to a {@link ManagedList} if necessary.
     *
     * @param list the original List
     * @return either the original list or a managed copy of it
     */
    private Object manageListIfNecessary(List<?> list) {
        boolean containsRuntimeRefs = false;
        for (Object element : list) {
            if (element instanceof RuntimeBeanReference) {
                containsRuntimeRefs = true;
                break;
            }
        }
        if (containsRuntimeRefs) {
            List<Object> managedList = new ManagedList<>();
            managedList.addAll(list);
            return managedList;
        }
        return list;
    }

    /**
     * This method overrides property setting in the scope of the {@code GroovyBeanDefinitionReader}
     * to set properties on the current bean definition.
     */
    @Override
    public void setProperty(String name, Object value) {
        if (this.currentBeanDefinition != null) {
            applyPropertyToBeanDefinition(name, value);
        }
    }

    protected void applyPropertyToBeanDefinition(String name, Object value) {
        if (value instanceof GString) {
            value = value.toString();
        }
        if (addDeferredProperty(name, value)) {
            return;
        }
        else if (value instanceof Closure) {
            GroovyBeanDefinitionWrapper current = this.currentBeanDefinition;
            try {
                Closure<?> callable = (Closure<?>) value;
                Class<?> parameterType = callable.getParameterTypes()[0];
                if (Object.class == parameterType) {
                    this.currentBeanDefinition = new GroovyBeanDefinitionWrapper("");
                    callable.call(this.currentBeanDefinition);
                }
                else {
                    this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(null, parameterType);
                    callable.call((Object) null);
                }

                value = this.currentBeanDefinition.getBeanDefinition();
            }
            finally {
                this.currentBeanDefinition = current;
            }
        }
        this.currentBeanDefinition.addProperty(name, value);
    }

    /**
     * This method overrides property retrieval in the scope of the
     * {@code GroovyBeanDefinitionReader}. A property retrieval will either:
     * <ul>
     * <li>Retrieve a variable from the bean builder's binding if it exists
     * <li>Retrieve a RuntimeBeanReference for a specific bean if it exists
     * <li>Otherwise just delegate to MetaClass.getProperty which will resolve
     * properties from the {@code GroovyBeanDefinitionReader} itself
     * </ul>
     */
    @Override
    public Object getProperty(String name) {
        Binding binding = getBinding();
        if (binding != null && binding.hasVariable(name)) {
            return binding.getVariable(name);
        }
        else {
            if (this.namespaces.containsKey(name)) {
                return createDynamicElementReader(name);
            }
            if (getRegistry().containsBeanDefinition(name)) {
                GroovyBeanDefinitionWrapper beanDefinition = (GroovyBeanDefinitionWrapper)
                        getRegistry().getBeanDefinition(name).getAttribute(GroovyBeanDefinitionWrapper.class.getName());
                if (beanDefinition != null) {
                    return new GroovyRuntimeBeanReference(name, beanDefinition, false);
                }
                else {
                    return new RuntimeBeanReference(name, false);
                }
            }
            // This is to deal with the case where the property setter is the last
            // statement in a closure (hence the return value)
            else if (this.currentBeanDefinition != null) {
                MutablePropertyValues pvs = this.currentBeanDefinition.getBeanDefinition().getPropertyValues();
                if (pvs.contains(name)) {
                    return pvs.get(name);
                }
                else {
                    DeferredProperty dp = this.deferredProperties.get(this.currentBeanDefinition.getBeanName() + name);
                    if (dp != null) {
                        return dp.value;
                    }
                    else {
                        return getMetaClass().getProperty(this, name);
                    }
                }
            }
            else {
                return getMetaClass().getProperty(this, name);
            }
        }
    }

    private GroovyDynamicElementReader createDynamicElementReader(String namespace) {
        XmlReaderContext readerContext = this.groovyDslXmlBeanDefinitionReader.createReaderContext(
                new DescriptiveResource("Groovy"));
        BeanDefinitionParserDelegate delegate = new BeanDefinitionParserDelegate(readerContext);
        boolean decorating = (this.currentBeanDefinition != null);
        if (!decorating) {
            this.currentBeanDefinition = new GroovyBeanDefinitionWrapper(namespace);
        }
        return new GroovyDynamicElementReader(namespace, this.namespaces, delegate, this.currentBeanDefinition, decorating) {
            @Override
            protected void afterInvocation() {
                if (!this.decorating) {
                    currentBeanDefinition = null;
                }
            }
        };
    }


    /**
     * This class is used to defer the adding of a property to a bean definition
     * until later. This is for a case where you assign a property to a list that
     * may not contain bean references at that point of assignment, but may later;
     * hence, it would need to be managed.
     */
    private static class DeferredProperty {

        private final GroovyBeanDefinitionWrapper beanDefinition;

        private final String name;

        public Object value;

        public DeferredProperty(GroovyBeanDefinitionWrapper beanDefinition, String name, Object value) {
            this.beanDefinition = beanDefinition;
            this.name = name;
            this.value = value;
        }

        public void apply() {
            this.beanDefinition.addProperty(this.name, this.value);
        }
    }


    /**
     * A RuntimeBeanReference that takes care of adding new properties to runtime references.
     */
    private class GroovyRuntimeBeanReference extends RuntimeBeanReference implements GroovyObject {

        private final GroovyBeanDefinitionWrapper beanDefinition;

        private MetaClass metaClass;

        public GroovyRuntimeBeanReference(String beanName, GroovyBeanDefinitionWrapper beanDefinition, boolean toParent) {
            super(beanName, toParent);
            this.beanDefinition = beanDefinition;
            this.metaClass = InvokerHelper.getMetaClass(this);
        }

        @Override
        public MetaClass getMetaClass() {
            return this.metaClass;
        }

        @Override
        public void setMetaClass(MetaClass metaClass) {
            this.metaClass = metaClass;
        }

        @Override
        public Object getProperty(String property) {
            if (property.equals("beanName")) {
                return getBeanName();
            }
            else if (property.equals("source")) {
                return getSource();
            }
            else if (this.beanDefinition != null) {
                return new GroovyPropertyValue(
                        property, this.beanDefinition.getBeanDefinition().getPropertyValues().get(property));
            }
            else {
                return this.metaClass.getProperty(this, property);
            }
        }

        @Override
        public Object invokeMethod(String name, Object args) {
            return this.metaClass.invokeMethod(this, name, args);
        }

        @Override
        public void setProperty(String property, Object newValue) {
            if (!addDeferredProperty(property, newValue)) {
                this.beanDefinition.getBeanDefinition().getPropertyValues().add(property, newValue);
            }
        }


        /**
         * Wraps a bean definition property and ensures that any RuntimeBeanReference
         * additions to it are deferred for resolution later.
         */
        private class GroovyPropertyValue extends GroovyObjectSupport {

            private final String propertyName;

            private final Object propertyValue;

            public GroovyPropertyValue(String propertyName, Object propertyValue) {
                this.propertyName = propertyName;
                this.propertyValue = propertyValue;
            }

            @SuppressWarnings("unused")
            public void leftShift(Object value) {
                InvokerHelper.invokeMethod(this.propertyValue, "leftShift", value);
                updateDeferredProperties(value);
            }

            @SuppressWarnings("unused")
            public boolean add(Object value) {
                boolean retVal = (Boolean) InvokerHelper.invokeMethod(this.propertyValue, "add", value);
                updateDeferredProperties(value);
                return retVal;
            }

            @SuppressWarnings("unused")
            public boolean addAll(Collection<?> values) {
                boolean retVal = (Boolean) InvokerHelper.invokeMethod(this.propertyValue, "addAll", values);
                for (Object value : values) {
                    updateDeferredProperties(value);
                }
                return retVal;
            }

            @Override
            public Object invokeMethod(String name, Object args) {
                return InvokerHelper.invokeMethod(this.propertyValue, name, args);
            }

            @Override
            public Object getProperty(String name) {
                return InvokerHelper.getProperty(this.propertyValue, name);
            }

            @Override
            public void setProperty(String name, Object value) {
                InvokerHelper.setProperty(this.propertyValue, name, value);
            }

            private void updateDeferredProperties(Object value) {
                if (value instanceof RuntimeBeanReference) {
                    deferredProperties.put(beanDefinition.getBeanName(),
                            new DeferredProperty(beanDefinition, this.propertyName, this.propertyValue));
                }
            }
        }
    }

}
